/**
 * @public
 * @constructor
 */
function GameLoop(gameEntitiesLayer0,gameEntitiesLayer1, inputQueue, gameLoopPerformanceTracker) {
	
	window.requestAnimationFrame = 
			window.requestAnimationFrame || /* Firefox 23 / IE 10 / Chrome */
			window.mozRequestAnimationFrame || /* Firefox < 23 */
			window.webkitRequestAnimationFrame || /* Safari */
			window.msRequestAnimationFrame || /* IE  */
			window.oRequestAnimationFrame; /* Opera */
	
	if(!window.requestAnimationFrame) {
		throw new Exception("Failed to get requestAnimationFrame funtion");
	}
	
	this.gameEntitiesLayer0 = gameEntitiesLayer0;
	this.gameEntitiesLayer1 = gameEntitiesLayer1;
	this.inputQueue = inputQueue;

	this.canvas = document.getElementById('canvas');
	this.context = this.canvas.getContext('2d');
	
	this.lastLoopCallTime = 0;
	this.accumulatedTimeMs = 0;
	
	this.perfTracker = gameLoopPerformanceTracker;
	
	/**
	 * @public
	 */
	this.start = function(canvasWidth, canvasHeight) {
		this.perfTracker.start();
		this.canvas.width=canvasWidth;
		this.canvas.height=canvasHeight;
		this.lastLoopCallTime = this.getCurrentTimeMs();
		this.update();
	};
	
	/**
	 * @private
	 */
	this.update = function() {
		/*
		 * - this function is first called by the start function (see above code)
		 * - since the start function is called on an instance of GameLoop, 
		 * at that time the "this" variable will represent the GameLoop instance
		 * - thus, when start first calls update, the "this" variable inside update will also
		 * represent the GameLoop instance
		 * - problem is, the last line of the update function makes a call to window.requestAnimationFrame 
		 * passing the update function as argument
		 * - when window.requestAnimationFrame calls (repeatedly) the update function, the "this" variable will represent
		 * window (not the GameLoop instance). 
		 */
		var self = this;
		
		var actualLoopDurationMs = self.getCurrentTimeMs()-self.lastLoopCallTime;
		self.lastLoopCallTime = self.getCurrentTimeMs();
		self.accumulatedTimeMs += actualLoopDurationMs;
		while(self.accumulatedTimeMs>=FIXED_STEP_IDEAL_DURATION_MS) {
			self.updateState();
			self.accumulatedTimeMs -= FIXED_STEP_IDEAL_DURATION_MS;
		}
		
		self.updateGraphics();
		
		window.requestAnimationFrame(function() {self.update();});
	};
	
	/**
	 * @private
	 */
	this.processInput = function() {
		while(!inputQueue.isEmpty()) {
			var event = inputQueue.pop();
			
			if(event instanceof ToggleDebugEvent) {
				DEBUG_MODE = event.value;
			}
			
			if(event instanceof MoveInputEvent) {
				ROUND_START_TIME = new Date().getTime();
			}
			
			for(var i=0; i<this.gameEntitiesLayer0.length; i++) {
				var gameEntity = this.gameEntitiesLayer0[i];
				gameEntity.processInput(event);
			}
			for(var i=0; i<this.gameEntitiesLayer1.length; i++) {
				var gameEntity = this.gameEntitiesLayer1[i];
				gameEntity.processInput(event);
			}
		}
	};
	
	/**
	 * @private
	 */
	this.updateState = function() {
		this.perfTracker.onGameLoopUpdateState();
		
		this.processInput();
		
		for(var i=0; i<this.gameEntitiesLayer0.length; i++) {
			var gameEntity = this.gameEntitiesLayer0[i];
			gameEntity.updateState();
		}
		for(var i=0; i<this.gameEntitiesLayer1.length; i++) {
			var gameEntity = this.gameEntitiesLayer1[i];
			gameEntity.updateState();
		}		
	};
	
	/**
	 * @private
	 */
	this.updateGraphics = function() {
		this.context.fillStyle = "white";
		this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
		
		this.perfTracker.onGameLoopUpdateGraphics();
		
		for(var i=0; i<this.gameEntitiesLayer0.length; i++) {
			var gameEntity = this.gameEntitiesLayer0[i];
			gameEntity.updateGraphics(this.context);
		}
		for(var i=0; i<this.gameEntitiesLayer1.length; i++) {
			var gameEntity = this.gameEntitiesLayer1[i];
			gameEntity.updateGraphics(this.context);
		}
	};	
	
	/**
	 * @private
	 */
	this.getCurrentTimeMs = function() {
		return new Date().getTime();
	};

};

function GameLoopPerformanceTracker(outputDivId) {
	
	this.jqueryOutputDiv = $('#'+outputDivId);
	
	this.globalStartTime = 0;
	this.localStartTime = 0;
	
	this.globalStateUpdatesCount = 0;
	this.globalGraphicsUpdatesCount = 0;
	
	this.localStateUpdatesCount = 0;
	this.localGraphicsUpdatesCount = 0;
	
	this.globalUps = 0;
	this.localUps = 0;
	this.globalFps = 0;
	this.localFps = 0;
	
	this.start = function() {
		this.globalStartTime = this.getTimestamp();
		this.localStartTime = this.getTimestamp();
		this.lastDisplayUpdateTime = this.getTimestamp();
	};
	
	this.onGameLoopUpdateState = function() {
		this.globalStateUpdatesCount++;
		this.localStateUpdatesCount++;
		
		var localTimeIntervalMs = this.getTimestamp()-this.localStartTime;
		if(localTimeIntervalMs > 500) {
			var globalTimeIntervalMs = this.getTimestamp()-this.globalStartTime;
			this.globalUps = (this.globalStateUpdatesCount*1000)/globalTimeIntervalMs;
			this.globalFps = (this.globalGraphicsUpdatesCount*1000)/globalTimeIntervalMs;	
			
			this.localUps = (this.localStateUpdatesCount*1000) / localTimeIntervalMs;  
			this.localFps = (this.localGraphicsUpdatesCount*1000) / localTimeIntervalMs;  
			
			this.localStateUpdatesCount = 0;
			this.localGraphicsUpdatesCount = 0;
			this.localStartTime = this.getTimestamp();
		}
	};
	
	this.onGameLoopUpdateGraphics = function() {
		this.globalGraphicsUpdatesCount++;
		this.localGraphicsUpdatesCount++;
		
		var output = "GLOBAL: FPS: "+this.formatNumber(this.globalFps)+" | UPS: "+this.formatNumber(this.globalUps)+" <br>" +
			         "LOCAL:  FPS: "+this.formatNumber(this.localFps)+" | UPS: "+this.formatNumber(this.localUps);
		
		this.jqueryOutputDiv.html(output);
	};
	
	this.getTimestamp = function() {
		return new Date().getTime();
	};
	
	this.formatNumber = function(number) {
		return new Number(number).toFixed(0);
	};
	
	
}